<!DOCTYPE html>
<html lang="en">
<head>
  <title>Build RESTful API - Born to build better enterprise frameworks and apps</title>
  <meta charset="utf-8">
  <meta name="description" content="index.description">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <link rel="icon" href="/images/favicon.png" type="image/x-icon">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.css" />
<link rel="stylesheet" href="/css/index.css">

    <script>
    !function(t,e,a,r,c){t.TracertCmdCache=t.TracertCmdCache||[],t[c]=window[c]||
      {_isInit:!0,call:function(){t.TracertCmdCache.push(arguments)},
      start:function(t){this.call('start',t)}},t[c].l=new Date;
      var n=e.createElement(a),s=e.getElementsByTagName(a)[0];
      n.async=!0,n.src=r,s.parentNode.insertBefore(n,s)}
    (window,document,'script','https://tracert.alipay.com/tracert.js','Tracert');
      Tracert.start({
        plugins: [ 'BucName' ],
        spmAPos: 'a454',
        spmBPos: 'b4893',
      });
    </script>
  
</head>
<body>
  <div class="nav" >
  <header>
    <a href="/en/" class="nav-logo leftpadding" alt="egg"><img src="https://zos.alipayobjects.com/rmsportal/VTcUYAaoKqXyHJbLAPyF.svg"></a>
    <ul class="nav-item">
      <li>
        <form id="search-form">
          <input type="text" id="search-query" class="search-query st-default-search-input">
        </form>
      </li>
      <li><a href="/en/intro/" alt="Guide">Guide</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/en/tutorials/index.html" alt="Tutorials">Tutorials</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="Plugins">Plugins</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="Release">Release</a></li>
      
      
        <li class="translations">
          <a class="nav-link">中文文档</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/tutorials/restful.html" style="color: #22ab28">English</a></li><li><a id="zh-cn" href="/zh-cn/tutorials/restful.html" >中文</a></li></ul>
        </li>
      
      <li><iframe src="https://ghbtns.com/github-btn.html?user=eggjs&repo=egg&type=star&count=true" frameborder="0" scrolling="0" width="150px" height="20px"></iframe></li>
    </ul>
    <a id="mobileTrigger" href="#" class="mobile-trigger">
      <ul>
        <li></li>
        <li></li>
        <li></li>
      </ul>
    </a>
  </header>
</div>
  <div id="container" class="container">
    <div class="page-main">
  <article class="markdown-body">
    <h1>Build RESTful API</h1>
    <p>Web frameworks are widely used for providing interfaces to the client through Web services. Let's use an example <a href="https://cnodejs.org/" target="_blank" rel="noopener">CNode Club</a> to show how to build <a href="https://en.wikipedia.org/wiki/REST" target="_blank" rel="noopener">RESTful</a> API using Egg.</p>
<p>CNode currently use v1 interface is not fully consistent with the RESTful semantic. In the article, we will encapsulate a more RESTful semantic V2 API based on CNode V1 interface.</p>
<h2 id="response-formatting"><a class="markdown-anchor" href="#response-formatting">#</a> Response Formatting</h2>
<p>Designing a RESTful-style API, we will identify the status of response by the response status code, keeping the response body simply and only the interface data is returned.
A example of <code>topics</code> is shown below:</p>
<h3 id="get-topics-list"><a class="markdown-anchor" href="#get-topics-list">#</a> Get topics list</h3>
<ul>
<li><code>GET /api/v2/topics</code></li>
<li>status code: 200</li>
<li>response body:</li>
</ul>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">[</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"id"</span>: <span class="string">"57ea257b3670ca3f44c5beb6"</span>,</span><br><span class="line">    <span class="attr">"author_id"</span>: <span class="string">"541bf9b9ad60405c1f151a03"</span>,</span><br><span class="line">    <span class="attr">"tab"</span>: <span class="string">"share"</span>,</span><br><span class="line">    <span class="attr">"content"</span>: <span class="string">"content"</span>,</span><br><span class="line">    <span class="attr">"last_reply_at"</span>: <span class="string">"2017-01-11T13:32:25.089Z"</span>,</span><br><span class="line">    <span class="attr">"good"</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">"top"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"reply_count"</span>: <span class="number">155</span>,</span><br><span class="line">    <span class="attr">"visit_count"</span>: <span class="number">28176</span>,</span><br><span class="line">    <span class="attr">"create_at"</span>: <span class="string">"2016-09-27T07:53:31.872Z"</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"id"</span>: <span class="string">"57ea257b3670ca3f44c5beb6"</span>,</span><br><span class="line">    <span class="attr">"author_id"</span>: <span class="string">"541bf9b9ad60405c1f151a03"</span>,</span><br><span class="line">    <span class="attr">"tab"</span>: <span class="string">"share"</span>,</span><br><span class="line">    <span class="attr">"content"</span>: <span class="string">"content"</span>,</span><br><span class="line">    <span class="attr">"title"</span>: <span class="string">"Finished Rewriting of Let's Learning Node.js Together"</span>,</span><br><span class="line">    <span class="attr">"last_reply_at"</span>: <span class="string">"2017-01-11T10:20:56.496Z"</span>,</span><br><span class="line">    <span class="attr">"good"</span>: <span class="literal">false</span>,</span><br><span class="line">    <span class="attr">"top"</span>: <span class="literal">true</span>,</span><br><span class="line">    <span class="attr">"reply_count"</span>: <span class="number">193</span>,</span><br><span class="line">    <span class="attr">"visit_count"</span>: <span class="number">47633</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>
<h3 id="retrieve-one-topic"><a class="markdown-anchor" href="#retrieve-one-topic">#</a> Retrieve one topic</h3>
<ul>
<li><code>GET /api/v2/topics/57ea257b3670ca3f44c5beb6</code></li>
<li>status code: 200</li>
<li>response body:</li>
</ul>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"id"</span>: <span class="string">"57ea257b3670ca3f44c5beb6"</span>,</span><br><span class="line">  <span class="attr">"author_id"</span>: <span class="string">"541bf9b9ad60405c1f151a03"</span>,</span><br><span class="line">  <span class="attr">"tab"</span>: <span class="string">"share"</span>,</span><br><span class="line">  <span class="attr">"content"</span>: <span class="string">"content"</span>,</span><br><span class="line">  <span class="attr">"title"</span>: <span class="string">"Finished Rewriting of Let's Learning Node.js Together"</span>,</span><br><span class="line">  <span class="attr">"last_reply_at"</span>: <span class="string">"2017-01-11T10:20:56.496Z"</span>,</span><br><span class="line">  <span class="attr">"good"</span>: <span class="literal">false</span>,</span><br><span class="line">  <span class="attr">"top"</span>: <span class="literal">true</span>,</span><br><span class="line">  <span class="attr">"reply_count"</span>: <span class="number">193</span>,</span><br><span class="line">  <span class="attr">"visit_count"</span>: <span class="number">47633</span>,</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="create-topics"><a class="markdown-anchor" href="#create-topics">#</a> Create topics</h3>
<ul>
<li><code>POST /api/v2/topics</code></li>
<li>status code: 201</li>
<li>response body:</li>
</ul>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"topic_id"</span>: <span class="string">"57ea257b3670ca3f44c5beb6"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="update-topics"><a class="markdown-anchor" href="#update-topics">#</a> Update topics</h3>
<ul>
<li><code>PUT /api/v2/topics/57ea257b3670ca3f44c5beb6</code></li>
<li>status code: 204</li>
<li>response body: null</li>
</ul>
<h3 id="error-handling"><a class="markdown-anchor" href="#error-handling">#</a> Error handling</h3>
<p>When an error is occurring, 4xx status code is returned if occurred by client-side request parameters and 5xx status code is returned if occurred by server-side logic processing. All error objects are used as the description for status exceptions.</p>
<p>For example, passing invalided parameters from the client may return a response with status code 422, the response body as shown below:</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line">&#123;</span><br><span class="line">  <span class="attr">"error"</span>: <span class="string">"Validation Failed"</span>,</span><br><span class="line">  <span class="attr">"detail"</span>: [ &#123; <span class="attr">"message"</span>: <span class="string">"required"</span>, <span class="attr">"field"</span>: <span class="string">"title"</span>, <span class="attr">"code"</span>: <span class="string">"missing_field"</span> &#125; ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="getting-started"><a class="markdown-anchor" href="#getting-started">#</a> Getting Started</h2>
<p>After interface convention, we begin to create a RESTful API.</p>
<h3 id="application-initialization"><a class="markdown-anchor" href="#application-initialization">#</a> Application initialization</h3>
<p>Initializes the application using <a href="https://github.com/eggjs/egg-init" target="_blank" rel="noopener">egg-init</a> in the <a href="../intro/quickstart.html">quickstart</a></p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ egg-init cnode-api --<span class="built_in">type</span>=simple</span><br><span class="line">$ <span class="built_in">cd</span> cnode-api</span><br><span class="line">$ npm i</span><br></pre></td></tr></table></figure>
<h3 id="enable-validate-plugin"><a class="markdown-anchor" href="#enable-validate-plugin">#</a> Enable validate plugin</h3>
<p><a href="https://github.com/eggjs/egg-validate" target="_blank" rel="noopener">egg-validate</a> is used to present the validate plugin.</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/plugin.js</span></span><br><span class="line">exports.validate = &#123;</span><br><span class="line">  enable: <span class="literal">true</span>,</span><br><span class="line">  package: <span class="string">'egg-validate'</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h3 id="router-registry"><a class="markdown-anchor" href="#router-registry">#</a> Router registry</h3>
<p>First of all, we follower previous design to register <a href="../basics/router.html">router</a>. The framework provides a simply way to create a RESTful-style router and mapping the resources to the corresponding controllers.</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/router.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">app</span> =&gt;</span> &#123;</span><br><span class="line">  app.router.resources(<span class="string">'topics'</span>, <span class="string">'/api/v2/topics'</span>, app.controller.topics);</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>Mapping the 'topics' resource's CRUD interfaces to the <code>app/controller/topics.js</code> using <code>app.resources</code></p>
<h3 id="developing-controller"><a class="markdown-anchor" href="#developing-controller">#</a> Developing controller</h3>
<p>In <a href="../basics/controller.html">controller</a>, we only need to implement the interface convention of <code>app.resources</code> <a href="../basics/router.html#RESTful-style-URL-definition">RESTful style URL definition</a>. For example, creating a 'topics' interface:</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/controller/topics.js</span></span><br><span class="line"><span class="keyword">const</span> Controller = <span class="built_in">require</span>(<span class="string">'egg'</span>).Controller;</span><br><span class="line"></span><br><span class="line"><span class="comment">// defining the rule of request parameters</span></span><br><span class="line"><span class="keyword">const</span> createRule = &#123;</span><br><span class="line">  accesstoken: <span class="string">'string'</span>,</span><br><span class="line">  title: <span class="string">'string'</span>,</span><br><span class="line">  tab: &#123; <span class="attr">type</span>: <span class="string">'enum'</span>, <span class="attr">values</span>: [ <span class="string">'ask'</span>, <span class="string">'share'</span>, <span class="string">'job'</span> ], <span class="attr">required</span>: <span class="literal">false</span> &#125;,</span><br><span class="line">  content: <span class="string">'string'</span>,</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TopicController</span> <span class="keyword">extends</span> <span class="title">Controller</span> </span>&#123;</span><br><span class="line">  <span class="keyword">async</span> create() &#123;</span><br><span class="line">    <span class="keyword">const</span> ctx = <span class="keyword">this</span>.ctx;</span><br><span class="line">    <span class="comment">// validate the `ctx.request.body` with the expected format</span></span><br><span class="line">    <span class="comment">// status = 422 exception will be thrown if not passing the parameter validation</span></span><br><span class="line">    ctx.validate(createRule);</span><br><span class="line">    <span class="comment">// call service to create a topic</span></span><br><span class="line">    <span class="keyword">const</span> id = <span class="keyword">await</span> ctx.service.topics.create(ctx.request.body);</span><br><span class="line">    <span class="comment">// configure the response body and status code</span></span><br><span class="line">    ctx.body = &#123;</span><br><span class="line">      topic_id: id,</span><br><span class="line">    &#125;;</span><br><span class="line">    ctx.status = <span class="number">201</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">module</span>.exports = TopicController;</span><br></pre></td></tr></table></figure>
<p>As shown above, a Controller mainly implements the following logic:</p>
<ol>
<li>call the validate function to validate the request parameters</li>
<li>create a topic by calling service encapsulates business logic using the validated parameters</li>
<li>configure the status code and context according to the interface convention</li>
</ol>
<h3 id="developing-service"><a class="markdown-anchor" href="#developing-service">#</a> Developing service</h3>
<p>We will more focus on writing effective business logic in <a href="../basics/service.html">service</a>.</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/service/topics.js</span></span><br><span class="line"><span class="keyword">const</span> Service = <span class="built_in">require</span>(<span class="string">'egg'</span>).Service;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TopicService</span> <span class="keyword">extends</span> <span class="title">Service</span> </span>&#123;</span><br><span class="line">  <span class="keyword">constructor</span>(ctx) &#123;</span><br><span class="line">    <span class="keyword">super</span>(ctx);</span><br><span class="line">    <span class="keyword">this</span>.root = <span class="string">'https://cnodejs.org/api/v1'</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">async</span> create(params) &#123;</span><br><span class="line">      <span class="comment">// call CNode V1 API</span></span><br><span class="line">    <span class="keyword">const</span> result = <span class="keyword">await</span> <span class="keyword">this</span>.ctx.curl(<span class="string">`<span class="subst">$&#123;<span class="keyword">this</span>.root&#125;</span>/topics`</span>, &#123;</span><br><span class="line">      method: <span class="string">'post'</span>,</span><br><span class="line">      data: params,</span><br><span class="line">      dataType: <span class="string">'json'</span>,</span><br><span class="line">      contentType: <span class="string">'json'</span>,</span><br><span class="line">    &#125;);</span><br><span class="line">    <span class="comment">// check whether the call was successful, throws an exception if it fails</span></span><br><span class="line">    <span class="keyword">this</span>.checkSuccess(result);</span><br><span class="line">    <span class="comment">// return the id of topis</span></span><br><span class="line">    <span class="keyword">return</span> result.data.topic_id;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Encapsulated a uniform check function, can be reused in query, create, update and such on in service</span></span><br><span class="line">  checkSuccess(result) &#123;</span><br><span class="line">    <span class="keyword">if</span> (result.status !== <span class="number">200</span>) &#123;</span><br><span class="line">      <span class="keyword">const</span> errorMsg = result.data &amp;&amp; result.data.error_msg ? result.data.error_msg : <span class="string">'unknown error'</span>;</span><br><span class="line">      <span class="keyword">this</span>.ctx.throw(result.status, errorMsg);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (!result.data.success) &#123;</span><br><span class="line">      <span class="comment">// remote response error</span></span><br><span class="line">      <span class="keyword">this</span>.ctx.throw(<span class="number">500</span>, <span class="string">'remote response error'</span>, &#123; <span class="attr">data</span>: result.data &#125;);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = TopicService;</span><br></pre></td></tr></table></figure>
<p>After developing the Service of topic creation, an interface have been completed from top to bottom.</p>
<h3 id="unified-error-handling"><a class="markdown-anchor" href="#unified-error-handling">#</a> Unified error handling</h3>
<p>Normal business logic has been completed, but exceptions have not yet been processed. Controller and Service may throw an exception as the previous coding, so it is recommended that throwing an exception to interrupt if passing invalided parameters from the client or calling the back-end service with exception.</p>
<ul>
<li>use Controller <code>this.ctx.validate()</code> to validate the parameters, throw exception if it fails.</li>
<li>call Service <code>this.ctx.curl()</code> to access CNode API, may throw server exception due to network problems.</li>
<li>an exception also will be thrown after Service is getting the response of calling failure from CNode API.</li>
</ul>
<p>Default error handling is provided but might be inconsistent as the interface convention previously. We need to implement a unified error-handling middleware to handle the errors.</p>
<p>Create a file <code>error_handler.js</code> under <code>app/middleware</code> directory to create a new <a href="../basics/middleware.html">middleware</a></p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// app/middleware/error_handler.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = <span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">async</span> <span class="function"><span class="keyword">function</span> <span class="title">errorHandler</span>(<span class="params">ctx, next</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="keyword">await</span> next();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">      <span class="comment">// All exceptions will trigger an error event on the app and the error log will be recorded</span></span><br><span class="line">      ctx.app.emit(<span class="string">'error'</span>, err, ctx);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> status = err.status || <span class="number">500</span>;</span><br><span class="line">      <span class="comment">// error 500 not returning to client when in the production environment because it may contain sensitive information</span></span><br><span class="line">      <span class="keyword">const</span> error = status === <span class="number">500</span> &amp;&amp; ctx.app.config.env === <span class="string">'prod'</span></span><br><span class="line">        ? <span class="string">'Internal Server Error'</span></span><br><span class="line">        : err.message;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// Reading from the properties of error object and set it to the response</span></span><br><span class="line">      ctx.body = &#123; error &#125;;</span><br><span class="line">      <span class="keyword">if</span> (status === <span class="number">422</span>) &#123;</span><br><span class="line">        ctx.body.detail = err.errors;</span><br><span class="line">      &#125;</span><br><span class="line">      ctx.status = status;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<p>We can catch all exceptions and follow the expected format to encapsulate the response through the middleware. It can be loaded into application using configuration file (<code>config/config.default.js</code>)</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// config/config.default.js</span></span><br><span class="line"><span class="built_in">module</span>.exports = &#123;</span><br><span class="line">  <span class="comment">// load the errorHandler middleware</span></span><br><span class="line">  middleware: [ <span class="string">'errorHandler'</span> ],</span><br><span class="line">  <span class="comment">// only takes effect on URL prefix with '/api'</span></span><br><span class="line">  errorHandler: &#123;</span><br><span class="line">    match: <span class="string">'/api'</span>,</span><br><span class="line">  &#125;,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>
<h2 id="testing"><a class="markdown-anchor" href="#testing">#</a> Testing</h2>
<p>Completing the coding just the first step, furthermore we need to add <a href="../core/unittest.html">Unit Test</a> to the code.</p>
<h3 id="controller-testing"><a class="markdown-anchor" href="#controller-testing">#</a> Controller Testing</h3>
<p>Let's start writing the unit test for the Controller. We can simulate the implementation of the Service layer in an appropriate way because the most important part is to test the logic as for Controller. And mocking up the Service layer according the convention of interface, so we can develop layered testing because the Service layer itself can also covered by Service unit test.</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; app, mock, assert &#125; = <span class="built_in">require</span>(<span class="string">'egg-mock/bootstrap'</span>);</span><br><span class="line"></span><br><span class="line">describe(<span class="string">'test/app/controller/topics.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="comment">// test the response of passing the error parameters</span></span><br><span class="line">  it(<span class="string">'should POST /api/v2/topics/ 422'</span>, () =&gt; &#123;</span><br><span class="line">    app.mockCsrf();</span><br><span class="line">    <span class="keyword">return</span> app.httpRequest()</span><br><span class="line">      .post(<span class="string">'/api/v2/topics'</span>)</span><br><span class="line">      .send(&#123;</span><br><span class="line">        accesstoken: <span class="string">'123'</span>,</span><br><span class="line">      &#125;)</span><br><span class="line">      .expect(<span class="number">422</span>)</span><br><span class="line">      .expect(&#123;</span><br><span class="line">        error: <span class="string">'Validation Failed'</span>,</span><br><span class="line">        detail: [</span><br><span class="line">          &#123; <span class="attr">message</span>: <span class="string">'required'</span>, <span class="attr">field</span>: <span class="string">'title'</span>, <span class="attr">code</span>: <span class="string">'missing_field'</span> &#125;,</span><br><span class="line">          &#123; <span class="attr">message</span>: <span class="string">'required'</span>, <span class="attr">field</span>: <span class="string">'content'</span>, <span class="attr">code</span>: <span class="string">'missing_field'</span> &#125;,</span><br><span class="line">        ],</span><br><span class="line">      &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// mock up the service layer and test the response of normal request</span></span><br><span class="line">  it(<span class="string">'should POST /api/v2/topics/ 201'</span>, () =&gt; &#123;</span><br><span class="line">    app.mockCsrf();</span><br><span class="line">    app.mockService(<span class="string">'topics'</span>, <span class="string">'create'</span>, <span class="number">123</span>);</span><br><span class="line">    <span class="keyword">return</span> app.httpRequest()</span><br><span class="line">      .post(<span class="string">'/api/v2/topics'</span>)</span><br><span class="line">      .send(&#123;</span><br><span class="line">        accesstoken: <span class="string">'123'</span>,</span><br><span class="line">        title: <span class="string">'title'</span>,</span><br><span class="line">        content: <span class="string">'hello'</span>,</span><br><span class="line">      &#125;)</span><br><span class="line">      .expect(<span class="number">201</span>)</span><br><span class="line">      .expect(&#123;</span><br><span class="line">        topic_id: <span class="number">123</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>As the Controller testing above, we create an application using <a href="https://github.com/eggjs/egg-mock" target="_blank" rel="noopener">egg-mock</a> and simulate the client to send request through <a href="https://github.com/visionmedia/supertest" target="_blank" rel="noopener">SuperTest</a>. In the testing, we also simulate the response from Service layer to test the processing logic of Controller layer</p>
<h3 id="service-testing"><a class="markdown-anchor" href="#service-testing">#</a> Service Testing</h3>
<p>Unit Test of Service layer may focus on the coding logic. <a href="https://github.com/eggjs/egg-mock" target="_blank" rel="noopener">egg-mock</a> provides a quick method to test the Service by calling the test method in the Service, and SuperTest to simulate the client request is no longer needed.</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">const</span> &#123; app, mock, assert &#125; = <span class="built_in">require</span>(<span class="string">'egg-mock/bootstrap'</span>);</span><br><span class="line"></span><br><span class="line">describe(<span class="string">'test/app/service/topics.test.js'</span>, () =&gt; &#123;</span><br><span class="line">  <span class="keyword">let</span> ctx;</span><br><span class="line"></span><br><span class="line">  beforeEach(<span class="function"><span class="params">()</span> =&gt;</span> &#123;</span><br><span class="line">    <span class="comment">// create a global context object so that can call the service function on a ctx object</span></span><br><span class="line">    ctx = app.mockContext();</span><br><span class="line">  &#125;)</span><br><span class="line"></span><br><span class="line">  describe(<span class="string">'create()'</span>, () =&gt; &#123;</span><br><span class="line">    it(<span class="string">'should create failed by accesstoken error'</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// calling service method on ctx directly</span></span><br><span class="line">        <span class="keyword">await</span> ctx.service.topics.create(&#123;</span><br><span class="line">          accesstoken: <span class="string">'hello'</span>,</span><br><span class="line">          title: <span class="string">'title'</span>,</span><br><span class="line">          content: <span class="string">'content'</span>,</span><br><span class="line">        &#125;);</span><br><span class="line">      &#125; <span class="keyword">catch</span> (err) &#123;</span><br><span class="line">        assert(err.status === <span class="number">401</span>);</span><br><span class="line">        assert(err.message === <span class="string">'error accessToken'</span>);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">throw</span> <span class="string">'should not run here'</span>;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    it(<span class="string">'should create success'</span>, <span class="keyword">async</span> () =&gt; &#123;</span><br><span class="line">      <span class="comment">// not affect the normal operation of CNode by simulating the interface calling of CNode based on interface convention</span></span><br><span class="line">      <span class="comment">// app.mockHttpclient method can easily simulate the appliation's HTTP request</span></span><br><span class="line">      app.mockHttpclient(<span class="string">`<span class="subst">$&#123;ctx.service.topics.root&#125;</span>/topics`</span>, <span class="string">'POST'</span>, &#123;</span><br><span class="line">        data: &#123;</span><br><span class="line">          success: <span class="literal">true</span>,</span><br><span class="line">          topic_id: <span class="string">'5433d5e4e737cbe96dcef312'</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">      &#125;);</span><br><span class="line"></span><br><span class="line">      <span class="keyword">const</span> id = <span class="keyword">await</span> ctx.service.topics.create(&#123;</span><br><span class="line">        accesstoken: <span class="string">'hello'</span>,</span><br><span class="line">        title: <span class="string">'title'</span>,</span><br><span class="line">        content: <span class="string">'content'</span>,</span><br><span class="line">      &#125;);</span><br><span class="line">      assert(id === <span class="string">'5433d5e4e737cbe96dcef312'</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>In the testing of Service layer above,  we create a Context object using the <code>app.createContext()</code> which provided by egg-mock and call the Service method on Context object to test directly. It can use <code>app.mockHttpclient()</code> to simulate the response of calling HTTP request, which allows us to focus on the logic testing of Service layer without the impact of environment.</p>
<hr>
<p>See the full example at <a href="https://github.com/eggjs/examples/tree/master/cnode-api" target="_blank" rel="noopener">eggjs/examples/cnode-api</a>.</p>

  </article>
  <aside id="mobileAside" class="toc">
  <div class="mobile-menu">
    <ul>
      <li><a href="/en/intro/" alt="Guide">Guide</a></li><li><a href="/api/" alt="API">API</a></li><li><a href="/en/tutorials/index.html" alt="Tutorials">Tutorials</a></li><li><a href="https://github.com/search?q=topic%3Aegg-plugin&type=Repositories" alt="Plugins">Plugins</a></li><li><a href="https://github.com/eggjs/egg/releases" alt="Release">Release</a></li>
      
      
        <li class="translations">
          <a class="nav-link">中文文档</a>
          <span class="arrow"></span><ul id="dropdownContent" class="dropdown-content"><li><a id="en" href="/en/tutorials/restful.html" style="color: #22ab28">English</a></li><li><a id="zh-cn" href="/zh-cn/tutorials/restful.html" >中文</a></li></ul>
        </li>
      
    </ul>
  </div>
  <dl><dt id="title-Intro" style="cursor: pointer;" class="aside-title">Guide<a id="collapse-icon-Intro" class="icon opend"></a></dt><dd id=panel-Intro><ul><li><a href="/en/intro/index.html" class="menu-link">What is Egg?</a></li><li><a href="/en/intro/egg-and-koa.html" class="menu-link">Egg and Koa</a></li><li><a href="/en/intro/quickstart.html" class="menu-link">Quick Start</a></li><li><a href="/en/tutorials/progressive.html" class="menu-link">Progressive</a></li><li><a href="/en/migration.html" class="menu-link">Migration to 2.x</a></li></ul></dd><dt id="title-Basics" style="cursor: pointer;" class="aside-title">Basis Function<a id="collapse-icon-Basics" class="icon opend"></a></dt><dd id=panel-Basics><ul><li><a href="/en/basics/structure.html" class="menu-link">Directory Structure</a></li><li><a href="/en/basics/objects.html" class="menu-link">Built-in Objects</a></li><li><a href="/en/basics/env.html" class="menu-link">Environment</a></li><li><a href="/en/basics/config.html" class="menu-link">Configuration</a></li><li><a href="/en/basics/middleware.html" class="menu-link">Middleware</a></li><li><a href="/en/basics/router.html" class="menu-link">Router</a></li><li><a href="/en/basics/controller.html" class="menu-link">Controller</a></li><li><a href="/en/basics/service.html" class="menu-link">Service</a></li><li><a href="/en/basics/plugin.html" class="menu-link">Plugin</a></li><li><a href="/en/basics/schedule.html" class="menu-link">Schedule</a></li><li><a href="/en/basics/extend.html" class="menu-link">Extend</a></li><li><a href="/en/basics/app-start.html" class="menu-link">Custom Init</a></li></ul></dd><dt id="title-Core" style="cursor: pointer;" class="aside-title">Core<a id="collapse-icon-Core" class="icon opend"></a></dt><dd id=panel-Core><ul><li><a href="/en/core/development.html" class="menu-link">Development</a></li><li><a href="/en/core/unittest.html" class="menu-link">Unit Testing</a></li><li><a href="/en/core/deployment.html" class="menu-link">Deployment</a></li><li><a href="/en/core/logger.html" class="menu-link">Logger</a></li><li><a href="/en/core/httpclient.html" class="menu-link">HttpClient</a></li><li><a href="/en/core/cookie-and-session.html" class="menu-link">Cookie and Session</a></li><li><a href="/en/core/cluster-and-ipc.html" class="menu-link">Cluster and IPC</a></li><li><a href="/en/core/view.html" class="menu-link">View</a></li><li><a href="/en/core/error-handling.html" class="menu-link">Error Handling</a></li><li><a href="/en/core/security.html" class="menu-link">Security</a></li><li><a href="/en/core/i18n.html" class="menu-link">i18n</a></li></ul></dd><dt id="title-Tutorials" style="cursor: pointer;" class="aside-title">Tutorials<a id="collapse-icon-Tutorials" class="icon opend"></a></dt><dd id=panel-Tutorials><ul><li><a href="/en/tutorials/mysql.html" class="menu-link">MySQL</a></li><li><a href="/en/tutorials/restful.html" class="menu-link">RESTful API</a></li><li><a href="/en/tutorials/passport.html" class="menu-link">Passport</a></li><li><a href="/en/tutorials/socketio.html" class="menu-link">Socket.IO</a></li><li><a href="/en/tutorials/assets.html" class="menu-link">Assets</a></li><li><a href="/en/tutorials/typescript.html" class="menu-link">TypeScript</a></li></ul></dd><dt id="title-Advanced" style="cursor: pointer;" class="aside-title">Advanced<a id="collapse-icon-Advanced" class="icon opend"></a></dt><dd id=panel-Advanced><ul><li><a href="/en/advanced/loader.html" class="menu-link">Loader</a></li><li><a href="/en/advanced/plugin.html" class="menu-link">Plugin Development</a></li><li><a href="/en/advanced/framework.html" class="menu-link">Framework</a></li><li><a href="/en/advanced/cluster-client.html" class="menu-link">Cluster Enhancement</a></li><li><a href="/en/advanced/view-plugin.html" class="menu-link">View Plugin</a></li><li><a href="/en/style-guide.html" class="menu-link">Style Guide</a></li></ul></dd><dt id="title-Community" style="cursor: pointer;" class="aside-title">Community<a id="collapse-icon-Community" class="icon opend"></a></dt><dd id=panel-Community><ul><li><a href="/en/plugins/" class="menu-link">Plugin List</a></li><li><a href="/en/contributing.html" class="menu-link">Contributing</a></li><li><a href="/en/resource.html" class="menu-link">Resource</a></li><li><a href="/en/faq.html" class="menu-link">FAQ</a></li></ul></dd></dl>
</aside>
<script>
var mobileTrigger = document.getElementById('mobileTrigger');
var mobileAside = document.getElementById('mobileAside');

var expandMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon opend';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = '';
  }
}

var collapseMenu = function(title) {
  // handle icon
  const collapseIcon = document.getElementById('collapse-icon-' + title);
  if (collapseIcon) {
    collapseIcon.className = 'icon closed';
  }
  // handle panelEle
  const panelEle = document.getElementById('panel-' + title);
  if (panelEle) {
    panelEle.className = 'aside-panel-hidden';
  }
}

mobileAside.onclick = function(e) {
  const targetId = e.target.id;
  if (targetId && (targetId.indexOf('title-') > -1 || targetId.indexOf('collapse-icon-') > -1)) {
    const title = targetId.replace('title-', '').replace('collapse-icon-', '');
    try { 
      // the the browser may have no localStroage or JSON.parse may throw exception.
      const menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
        
      // current menu status
      const curClosed = menuInfo[title] ? menuInfo[title].closed : false; // default false

      // change UI
      curClosed ? expandMenu(title) : collapseMenu(title);

      // save menuInfo to localStorage
      menuInfo[title] = { closed: !curClosed } // opposite
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    } catch (e) {}
  }
};

mobileTrigger.onclick = function(e) {
  e.preventDefault();
  if (mobileAside.className.indexOf('mobile-show') === -1) {
    mobileAside.className += ' mobile-show';
  } else {
    mobileAside.className = 'toc';
  }
};

(function() {
  // save data to localStorage because the page will refresh when user change the url.
  let menuInfo;
  try { 
    // the the browser may have no localStroage or JSON.parse may throw exception.
    menuInfo = JSON.parse(window.localStorage.getItem('menuInfo'));
    if (!menuInfo) {
      menuInfo = {};
      window.localStorage.setItem('menuInfo', JSON.stringify(menuInfo));
    }
  } catch (e) {
    menuInfo = {}; // default {}
  }

  for (const title in menuInfo) {
    if (menuInfo[title] && menuInfo[title].closed) { // menu in closed status.
      collapseMenu(title);
    } else {
      expandMenu(title);
    }
  }

  // highlight menu
  const pathname = window.location.pathname;
  const selector = `a[href="${pathname}"].menu-link,a[href="${pathname}index.html"].menu-link`;
  const menuItem = mobileAside.querySelector(selector);
  if (menuItem) { menuItem.className += ' highlight'; }
})();
</script>

</div>

  </div>
</body>
<script src="https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js"></script>
<script>
docsearch({
  apiKey: '1561de31a86f79507ea00cdb54ce647c',
  indexName: 'eggjs',
  inputSelector: '#search-query',
});
</script>
<div class="cnzz">
<script src="https://s11.cnzz.com/z_stat.php?id=1261142226&web_id=1261142226" language="JavaScript"></script>
</div>

</html>
